package com.kunpan.admin.common.utils.file;

import com.kunpan.admin.aop.AutoFillAspect;
import com.kunpan.admin.async.MinioFileUploadAsyncService;
import com.kunpan.admin.common.redis.RedisCache;
import com.kunpan.admin.entity.ChunkFile;
import com.kunpan.admin.exception.file.MinIOFileStorageException;
import com.kunpan.admin.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.DeleteObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static java.util.Map.*;

@Component
public class MinioUtils {

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioProperties minioProperties;

    @Autowired
    private MinioFileUploadAsyncService minioFileUploadAsyncService;

    private static final Logger log = LoggerFactory.getLogger(MinioUtils.class);

    /**
     * minio小文件上传
     */
    public String minioUpload(MultipartFile file, String fileName, String fileMd5) {
        StringBuilder sb = new StringBuilder();
        //获取文件类型字符串
        String fileType = MimeTypeUtils.FILE_TYPE[FileUtils.getMultipartFileType(fileName)];
        String path = sb.append(minioProperties.getUploadPath()).append(fileType).append("/").append(fileName).toString();
        sb.setLength(0); // 清空 StringBuilder 以便重用
        //开始使用minio上传文件
        try {
            minioClient.putObject(PutObjectArgs.builder().bucket(minioProperties.getBucketName())
                    .object(path)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //设置文件状态缓存,以防止重复上传操作
        redisCache.setStatus(fileMd5, 1, true);
        redisCache.expire(fileMd5, 30, TimeUnit.SECONDS);
        return sb.append(minioProperties.getUrl()).append("/").append(minioProperties.getBucketName()).append("/").append(path).toString();
    }

    /**
     * minio大文件分片上传
     */
    public void chunkUpload(MultipartFile file, String fileName, Integer chunkIndex, Integer shardTotal, String fileMd5) {
        // 生成分片的唯一标识
        String chunkKey = fileMd5 + "_chunk_" + chunkIndex;
        String cacheKey = "chunk_" + fileMd5;
        StringBuilder chunkSb = new StringBuilder();
        String path;
        if (redisCache.hasKey(cacheKey)) {//判断文件缓存中是否有当前分片文件的路径值
            path = redisCache.getCacheObject(cacheKey);
        } else {
            //根据文件类型生成对应的文件路径
            //获取文件类型字符串
            String fileType = MimeTypeUtils.FILE_TYPE[FileUtils.getMultipartFileType(fileName)];
            path = chunkSb.append(minioProperties.getUploadPath()).append(fileType).append("/").toString();
            redisCache.setCacheObject(cacheKey, path);
            chunkSb.setLength(0);
        }
        //上传分片文件
        try (InputStream inputStream = file.getInputStream()) {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(chunkSb.append(path).append(chunkKey).toString())
                            .stream(inputStream, file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());
        } catch (Exception e) {
            //出现异常时
            throw new RuntimeException(e);
        }
        //使用异步方式上传分片文件
//        minioFileUploadAsyncService.minioChunkUpload(file, fileName, chunkIndex, shardTotal, fileMd5);
    }

    public String mergeChunks(String fileMd5, Integer shardTotal, String fileName) throws Exception {
        // 生成合并后的文件唯一标识
        String cacheKey = "chunk_" + fileMd5;
        String path = "";
        if (redisCache.hasKey(cacheKey)) {
            path = redisCache.getCacheObject(cacheKey);
        }
        //String mergedFileKey = fileMd5 + "_merged_" + userId;
        StringBuilder mergeSb = new StringBuilder();
        //分片集合
        List<ComposeSource> sources = new ArrayList<>();
        List<DeleteObject> dos = new ArrayList<>();
        // 合并分片文件
        for (int i = 0; i < shardTotal; i++) {
            String chunkKeyPath = mergeSb.append(path).append(fileMd5).append("_chunk_").append(i).toString();
            //将当前分片文件添加到 sources 集合中
            sources.add(ComposeSource.builder().bucket(minioProperties.getBucketName()).object(chunkKeyPath).build());
            dos.add(new DeleteObject(chunkKeyPath));
            //重置当前mergeSb中的字符串
            mergeSb.setLength(0);
        }
        // 创建 ComposeObjectArgs 对象，并设置目标 bucket 和 object 名称，以及分片文件列表
        String mergedFilePath = mergeSb.append(path).append(fileName).toString();
        minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .object(mergedFilePath)
                        .sources(sources)
                        .build());

        // 使用异步方式删除分片文件
        minioFileUploadAsyncService.minioChunkDelete(dos);
        //删除文件的同时删除当前文件存在缓存中的路径数据
        redisCache.deleteObject(cacheKey);
        //合并完成后删除文件状态缓存
        redisCache.deleteStatus(fileMd5);
        mergeSb.setLength(0);
        return mergeSb.append(minioProperties.getUrl()).append("/").append(minioProperties.getBucketName()).append(mergedFilePath).toString();
    }

    /**
     * 生成预签名下载URL
     */
    public String generatePresignedUrl(String objectName,Long fileSize) {
        try {
            Map<String, String> map = new HashMap<>();
            map.put("response-content-disposition", "attachment");
            map.put("X-Amz-Max-Downloads", "3");//限制此url只能下载3次
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(minioProperties.getBucketName())
                            .object(objectName.split(minioProperties.getBucketName() + "/")[1])
                            .expiry((int)calculateExpiry(fileSize))
                            .extraQueryParams(map)
                            .build()
//                            .expiry((int)calculateExpiry(fileSize))
            );
        } catch (Exception e) { // 捕获所有 MinIO 客户端异常
            // 记录详细日志
            log.error("生成预签名 URL 失败 | 文件: {} | 错误原因: {}", objectName, e.getMessage(), e);
            // 封装为自定义异常，传递原始异常堆栈
            throw new MinIOFileStorageException("文件服务暂不可用", e);
        }
    }

    // 动态有效期计算策略
    private long calculateExpiry(long fileSizeBytes) {
        // 基础参数配置（可提取到配置文件中）
        final double AVG_DOWNLOAD_SPEED_KB = 180.0; // 假设平均下载速度180kb,
        final double SAFETY_FACTOR = 2.5; // 安全系数
        final int MIN_EXPIRY = 60; // 最小有效期60秒
        final int MAX_EXPIRY = 3600 * 6; // 最大有效期6小时

        // 计算理论下载时间（秒）
        double fileSizeKB = fileSizeBytes / 1024.0;
        //根据文件大小计算理论下载时间
        double theoreticalTime = fileSizeKB / AVG_DOWNLOAD_SPEED_KB;

        // 应用安全系数并取整
        long calculatedTime = (long) (theoreticalTime * SAFETY_FACTOR);

        // 边界约束
        return Math.min(MAX_EXPIRY, Math.max(MIN_EXPIRY, calculatedTime));
    }

    /**
     * 将文件大小记录到缓存中
     */
    public void recordFileSize(String key, Long fileSize) {
        redisCache.setCacheObject(key, fileSize);
    }

    /**
     * 获取文件在缓存中的记录
     */
    public Long getFileSize(String key) {
        if (redisCache.hasKey(key)) {
            return redisCache.getCacheObject(key);
        }
        return null;
    }

    /**
     * 将文件大小记录从缓存中删除
     */
    public Boolean deleteFileSize(String key) {
        if (redisCache.hasKey(key)) {
            return redisCache.deleteObject(key);
        }
        return false;
    }
}
